Python is installed on our department's systems.
Download and install python on your own computer by following instructions at http://www.python.org or install the Anaconda distribution.
On our systems, enter the ipython
interactive environment with
ipython
To quit, type control-d
To run python code in file code.py
, either type
run code.py
in ipython
, or type
python code.py
at the unix command prompt.
When in the ipython
, you may type python statements or expressions
that are evaluated, or ipython
commands. See the
Video
tutorial on using ipython, in five parts by Jeff Rush, for help
getting started with ipython
.
Here we will use the jupyter notebook to provide examples that you can download and run.
x = 33
y = 432.1
x + y
who
whos
me = 'chuck'
you = 'jill'
whos
me + you
me + ' ' + you
def add(x, increment=1):
'''This is a very powerful addition function.
Usage:
>>> add(10)
11
>>> add(10, 3)
13'''
return x + increment
add?
add(30)
add(30, 100)
Let's say you need to know, approximately, the roots of $x^3 - 4x^2 -11x + 30$. What are they?
You have a fast computer at your disposal, and python is installed on it. What do you do?
Define a function named f
that takes one argument x
and
returns the value of the above polynomial.
def f(x):
return x**3 - 4 * x**2 - 11 * x + 30
f(2.3)
Now we need to figure out how to generate a random number, say between -10
and 10, in python. Try searching the net for python random number
. You soon discover the random
module.
Modules in python are defined by python source files. The random
module is defined in a file named random.py
. Where might it be on
our system? Try
locate random.py
On my computer I see it is in /usr/lib64/python3.5
. Take a look at
the file contents.
To use this module, you must first import it.
import random
This interprets the contents of the file, defining variables, functions, and classes for your use during the python session.
On the random
module web page we found, you can read through the
available functions, such as uniform(a,b)
which generates a
pseudo-random number between a
and b
. Call it like
randomNumber = random.uniform(-10, 10)
because uniform
lives in the module name's namespace.
Now we need a way to loop a number of times, each time generating a
new pseudo-random number and testing it as the argument to our
polynomial function f
. We can use a for loop, like
for i in range(5):
print(i)
range(5)
produces a generator for the sequence 0, 1, 2, 3, 4
, so the same loop could
be written
for i in [0, 1, 2, 3, 4]:
print(i)
Now, we are ready to try a bunch of random argument values.
import random
for i in range(5):
x = random.uniform(-10, 10)
fx = f(x)
print(x, fx)
Humm...we need to try many more $x$ values to find some for which $f(x)\approx 0$. And we must keep track of the $x$ value for which $|f(x)|$ is the closest to zero. We could use two variables for this, one for the best $x$ and $f(x)$ so far, but let's do this in a more pythonic way...as a pair, or a tuple, like
bestSoFar = (x,fx)
The elements of a tuple cannot be modified, but they can be accessed by
bestSoFar[0] # x
bestSoFar[1] # f(x)
Lists can also be accessed this way. Multiple elements can be accessed with slices.
nums = [0, 1, 2, 3, 4, 5]
nums
nums[2:4]
nums[:3]
nums[2:]
Okay, so now we have x
, fx
, and bestSoFar
. How do we test
to see if we have a new best result and then update bestSoFar
?
Develop your python thoughts in small steps, testing each piece.
firstx = 0
bestSoFar = (firstx, f(firstx))
bestSoFar
newx = 3
fx = f(newx)
fx
Which value for $x$ is better, 30 or -12?
abs(fx) < abs(bestSoFar[1])
So, we can update bestSoFar
using
if abs(fx) < abs(bestSoFar[1]):
bestSoFar = (x, fx)
It's just that simple.
Now, putting the pieces together, the complete python code to find the value of $x$ from a set of 100 random values for which $f(x)$ is closest to zero, we do the following.
import random
def f(x):
return x**3 - 4*x**2 - 11*x + 30
bestSoFar = (0, f(0))
for step in range(100):
x = random.uniform(-10, 10)
fx = f(x)
if (abs(fx) < abs(bestSoFar[1])):
bestSoFar = (x, fx)
bestSoFar
Not such a great root value. $f(x)$ is not very close to zero.
Let's try 10000 random numbers.
bestSoFar = (0, f(0))
for step in range(1000):
x = random.uniform(-10, 10)
fx = f(x)
if (abs(fx) < abs(bestSoFar[1])):
bestSoFar = (x, fx)
bestSoFar
That's better. Try again with more numbers? But this is getting tedious. Let's write a function to do this that takes one argument as the number of values of $x$ to try.
Also, let's include an argument that accepts the function for which we want to find a root. How do you pass a function into another function as an argument?
We will do this in class. First, download this jupyter notebook. Then, run
jupyter notebook
open this notebook, then scroll down to this cell and define and test the function.
def findRoot(f, xmin, xmax, n=1000):
bestSoFar = (0, f(0))
for step in range(n):
x = random.uniform(xmin, xmax)
fx = f(x)
if (abs(fx) < abs(bestSoFar[1])):
bestSoFar = (x, fx)
return bestSoFar
findRoot(f, -10, 10, 100000)
Now for some fun. Let's plot the progress of our search by collecting
the bestSoFar
values every time they are updated. How do we plot
in python? Again, you can search the net. To speed things up, here
is an example.
import matplotlib.pyplot as plt
plt.ion() #interactive plotting on
plt.plot(range(10), 'o-')
In this jupyter notebook, we will specify the plots to be inline, rather than calling plt.ion()
.
import matplotlib.pyplot as plt
%matplotlib inline
plt.plot(range(10), 'o-')
Ta-da! A plot! The x-axis values were assumed to be $0,\ldots,n$ where $n$ is the number of values given as the first argument.
Here is another example. Let's plot the values of the sine function,
which is defined in the numpy
module. We will also make use of the
linspace
function in the numpy
module to generate some $x$
values, and the sin
function.
import numpy as np
xs = np.linspace(-10,10,100)
plt.plot(xs,np.sin(xs))
If we were working from inside the ipython
environment, this adds the sine curve to the current plot. We could first clear
the figure by doing
plt.clf()
Now, to collect the bestSoFar
values. Let's just build up a list
called trace
by adding the new value of bestSoFar
whenever it
is changed. Can use the append
list method for this.
trace.append(bestSoFar)
assuming trace
was initialized.
Here is a function for this. Let's put it with the import statements
in a python source file, named findRoot.py
. Notice that the function returns two things, the
bestSoFar
pair, and the trace
, as a tuple.
import random
import matplotlib.pyplot as plt
def findRoot(f, xmin, xmax, maxSteps=1000):
bestSoFar = (0, f(0))
trace = [bestSoFar]
for step in range(maxSteps):
x = random.uniform(xmin, xmax)
fx = f(x)
if (abs(fx) < abs(bestSoFar[1])):
bestSoFar = (x,fx)
trace.append(bestSoFar)
return (bestSoFar, trace)
findRoot(f, -10, 10, 100)
Now we see this function returned the best guess at the root, but it also returned the sequence of best guesses so far.
From this notebook, we can write this code into a file named findRoot.py
.
%%writefile findRoot.py
import random
import matplotlib.pyplot as plt
def findRoot(f, xmin, xmax, maxSteps=1000):
bestSoFar = (0, f(0))
trace = [bestSoFar]
for step in range(maxSteps):
x = random.uniform(xmin, xmax)
fx = f(x)
if (abs(fx) < abs(bestSoFar[1])):
bestSoFar = (x,fx)
trace.append(bestSoFar)
return (bestSoFar, trace)
This file can be loaded
into ipython
and used as shown here.
import findRoot
def f(x):
return x**3 - 4 * x**2 - 11 * x + 30
findRoot.findRoot(f, -10, 10, 1000)
What we want to plot is the second of each pair in the list that is the second thing returned.
Hummm...here is a chance to show the beauty of list comprehensions. Remember set notation? What is the set $\{x^2 | x \in \mathcal{N}, x < 10\}$? List comprehensions mimic this notation. The same set in python is
[x**2 for x in range(10)]
Now, if we assign the result of findRoot
, like
result = findRoot.findRoot(f, -10, 10, 10000)
we can collect just the best $f(x)$ values by
values = [a[1] for a in result[1]]
values
and these can be plotted like
plt.plot(values, 'o-')
plt.plot([0, len(values)-1], [0, 0], 'r--')
We computer scientists are all about testing our code, right? So, for every module (meaning every python source file) you write, you should include some testing code. An easy way to do this is to add some statements at the end of the file that call the functions defined above. We don't want these called every time you import your file, though. Only when it is run, by doing
run findRoot
in ipython
, or by doing
python findRoot.py
from the unix command line. When your code is run by either of these,
the variable __name__
has the string value
__main__
, so the testing code can be in the true block of
an if statement that checks this. The whole file is now
%%writefile findRoot.py
import random
import matplotlib.pyplot as plt
def findRoot(f, xmin, xmax, maxSteps=1000):
bestSoFar = (0, f(0))
trace = [bestSoFar]
for step in range(maxSteps):
x = random.uniform(xmin, xmax)
fx = f(x)
if (abs(fx) < abs(bestSoFar[1])):
bestSoFar = (x, fx)
trace.append(bestSoFar)
return (bestSoFar, trace)
if __name__ == '__main__':
def f(x):
return x**3 - 4*x**2 - 11*x + 30
result = findRoot(f, -10, 10, 10000)
print(result)
values = [bests[1] for bests in result[1]]
plt.plot(values,'o-')
plt.plot([0, len(values)-1], [0, 0], 'r--')
import findRoot
run findRoot
Python inludes a very efficient implementation of associative maps, which are called dictionaries in python. Each entry has a key and a value. Keys must only be immutable objects.
Here is a dictionary for associating grades with people. The key is a string.
grades = {'Jim' : 88.2, 'Kim': 93, 'Slim': 75.2}
grades
grades['Jim']
grades['Kim'] = 94.2
grades
grades['Wim'] = 52
grades
grades['Nim']
for k, v in grades.items():
print(k,v)
grades.get('Nim', 'grade missing')
Most everything in python is a reference, except primitive types. So
x = [1, 2]
x
y = x
y
y.append(3)
y
x
Careful, though. When concatenating two lists with +
, copies are
made.
x = [1, 2]
x
y = x
y
y = y + [3]
y
x
With primitive types, the result is more what you would expect.
x = 42
x
y = x
y
y += 1
y
x
Therefore, passing arguments to functions is by reference except for primitive types.
def changeInt(x):
x = x + 2
print('In changeInt', x)
def changeStr(s):
s = s + ' you'
print('In changeStr', s)
def changeList(lst):
lst = lst + [23, 52.0]
print('In changeList', lst)
def changeList2(lst):
lst.append([23, 52.0])
print('In changeList2', lst)
def changeDict(dict):
dict['newone'] = 42
print('In changeDict', dict)
num = 33
s = "hello"
lst = [1, 2, 3]
dict = {'a':34, 'b':22}
print(num)
changeInt(num)
print(num)
print()
print(s)
changeStr(s)
print(s)
print()
print(lst)
changeList(lst)
print(lst)
print()
print(lst)
changeList2(lst)
print(lst)
print()
print(dict)
changeDict(dict)
print(dict)
print()
Say you are editing some python code in a file named search.py
and
you write some test code in a file named
testSearch.py
. You want to run testSearch.py
or use
control-c control-c
in emacs to repeatedly run the test code after
editing search.py
. You must force python to reload the search
module by starting testSearch.py
with
import search
import imp
imp.reload(search)
In most languages, since
x = 20
def addTen():
print(x + 10)
addTen()
works, you would expect
x = 20
def addTen():
x = x + 10
to work, too, but
addTen()
What's going on?
The assignment to x
in the second function forces x
to be
local to the function, therefore its use on the right-hand side is
referencing a local variable that does not have a value yet. The
first version of the function does not assign to x
so uses the
reference to x
defined outside the function. If you want to
change x
in the outer environment, then you must set its value
using a returned result, or add a global
statement.
def addTen():
return x + 10
x
x = addTen()
x
Watch out! This still messes me up from time to time.
Data structures like lists are easily changed in a function. Here is one.
def removeLast(stuff):
return stuff.pop()
nums = range(10)
nums
nums = list(range(10))
nums
removeLast(nums)
removeLast(nums)
removeLast(nums)
nums
Now let's try appending elements.
def addToEnd(stuff, item):
stuff.append(item)
addToEnd(nums, 10)
nums
Okay. Let's add more than one element.
def addToEnd(stuff, items):
stuff = stuff + items # or stuff += items
addToEnd(nums, [20, 21])
nums
Rats! What's going on?
The +
operator on lists creates a new list rather than modifying
the old list. And the reference to the new list is assigned to the
local variable stuff
. So, stuff
first referred to the
nums
list which was used on the right hand side of the assignment,
but then stuff
was reassigned to refer to the new list. What do
you do if you want to change nums
?
def addToEnd(stuff, items):
return stuff + items
addToEnd(nums, [20, 21])
Oops. Oh yeah . . .
nums = addToEnd(nums, [20, 21])
nums
Let's say we use a list to represent the state of a search problem. We write a function to modify the state in several ways, to represent the successors of that state. As an example, let's just add 1 to an element.
def addOne(state, index):
state[index] = state[index] + 1 # or state[index] += 1
state = [1, 2, 3]
state
addOne(state, 0)
state
That works. But now state
does not have its original value.
addOne(state, 1)
state
If you want to keep it, you can always store the original state in a separate variable, right?
origState = [1, 2, 3]
state = origState
addOne(state, 0)
state
origState
Doh! origState
and state
are references to the same object.
Solution is to use the copy
function. (See the documentation).
import copy
origState = [1, 2, 3]
state = copy.copy(origState)
addOne(state, 0)
state
origState
Now, let's get fancy and use a list comprehension to create a list of all ways of adding 1 to the elements of the state.
[addOne(copy.copy(state), i) for i in range(len(state))]
Arrgh! What happened? None
was returned by addOne
. Why?
Because we didn't return anything!
Try again.
def addOne(state, index):
state[index] += 1
return state
state = [1, 2, 3]
[addOne(copy.copy(state), i) for i in range(len(state))]
Which end of a list is it faster to pop
from? Remember your linked lists?
Use the ipython
magic function timeit
.
def testPopFront():
x = list(range(10000))
for i in range(100):
x.pop(0)
def testPopBack():
x = list(range(10000))
for i in range(100):
x.pop()
timeit testPopFront()
timeit testPopBack()
Popping from the back is faster, about twice as fast.